package cardgame;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import com.jcraft.jogg.Packet;
import com.jcraft.jogg.Page;
import com.jcraft.jogg.StreamState;
import com.jcraft.jogg.SyncState;
import com.jcraft.jorbis.Block;
import com.jcraft.jorbis.Comment;
import com.jcraft.jorbis.DspState;
import com.jcraft.jorbis.Info;

/**
 * Simple Clip like player for OGG's. Code is mostly taken from the example provided with 
 * JOrbis.
 * 
 * @author kevin
 */
public class OggClip{
    
    private final int BUFSIZE = 4096 * 2;
    private int convsize = BUFSIZE * 2;
    private byte[] convbuffer = new byte[convsize];
    private SyncState oy;
    private StreamState os;
    private Page og;
    private Packet op;
    private Info vi;
    private Comment vc;
    private DspState vd;
    private Block vb;
    private SourceDataLine outputLine;
    private int rate;
    private int channels;
    private BufferedInputStream bitStream=null;
    private byte[] buffer=null;
    private int bytes=0;
    private Thread player=null;

    private float balance;
    private float gain = -1;
    private boolean paused;
    private float oldGain;

    /**
     * Create a new clip based on a reference into the class path
     * 
     * @param resourcePath The reference into the class path which the ogg can be read from
     * @throws IOException Indicated a failure to find the resource
     */
    public OggClip(String resourcePath){
        init(Util.getResourceInputStream(resourcePath));
    }

    /**
     * Create a new clip based on a reference into the class path
     * 
     * @param in The stream from which the ogg can be read from
     * @throws IOException Indicated a failure to read from the stream
     */
    public OggClip(InputStream in) throws IOException{
        init(in);
    }

    /**
     * Set the default gain value (default volume)
     */
    public void setDefaultGain(){
        setGain(-1);
    }

    /**
     * Attempt to set the global gain (volume ish) for the play back. If the control is not supported
     * this method has no effect. 1.0 will set maximum gain, 0.0 minimum gain
     * 
     * @param gain The gain value
     */
    public void setGain(float gain) {
        if(gain != -1){
            if((gain < 0) || (gain > 1)){
                throw new IllegalArgumentException("Volume must be between 0.0 and 1.0");
            }
        }
        this.gain = gain;
        if(outputLine == null){
            return;
        }
        try{
            FloatControl control = (FloatControl) outputLine.getControl(FloatControl.Type.MASTER_GAIN);
            if(gain == -1){
                control.setValue(0);
            }
            else{
                float max = control.getMaximum();
                float min = control.getMinimum(); // negative values all seem to be zero?
                float range = max - min;
                control.setValue(min+(range*gain));
            }
        }catch(IllegalArgumentException ex){
            // gain not supported
            ex.printStackTrace();
        }
    }

    /**
     * Attempt to set the balance between the two speakers. -1.0 is full left speak, 1.0 if full right speaker.
     * Anywhere in between moves between the two speakers. If the control is not supported
     * this method has no effect
     * 
     * @param balance The balance value
     */
    public void setBalance(float balance){
        this.balance = balance;
        if(outputLine == null){
            return;
        }
        try{
            FloatControl control = (FloatControl) outputLine.getControl(FloatControl.Type.BALANCE);
            control.setValue(balance);
        }catch(IllegalArgumentException ex){
            // balance not supported
        }
    }

    /**
     * Check the state of the play back
     * 
     * @return True if the playback has been stopped
     */
    private boolean checkState(){
        while(paused && (player != null)){
            synchronized(player){
                if(player != null){
                    try{
                        player.wait();
                    }catch (InterruptedException ex){
                        // ignored
                    }
                }
            }
        }
        return stopped();
    }

    /**
     * Pause the play back
     */
    public void pause(){
        paused = true;
        oldGain = gain;
        setGain(0);
    }

    /**
     * Check if the stream is paused
     * 
     * @return True if the stream is paused
     */
    public boolean isPaused(){
        return paused;
    }

    /**
     * Resume the play back
     */
    public void resume(){
        if(!paused){
            play();
            return;
        }
        paused = false;
        synchronized(player){
            if(player != null){
                player.notify();
            }
        }
        setGain(oldGain);
    }

    /**
     * Check if the clip has been stopped
     * 
     * @return True if the clip has been stopped
     */
    public boolean stopped(){
        return ((player == null) || (!player.isAlive()));
    }

    /**
     * Initialise the ogg clip
     * 
     * @param in The stream we're going to read from
     * @throws IOException Indicates a failure to read from the stream
     */
    private void init(InputStream in){
        bitStream = new BufferedInputStream(in);
        bitStream.mark(Integer.MAX_VALUE);
    }

    /**
     * Play the clip once
     */
    public void play(){
        stop();
        try{
            bitStream.reset();
        }catch(IOException ex){
            // ignore if no mark
        }
        player = new Thread(){
            
            @Override
            public void run(){
                try{
                    playStream(Thread.currentThread());
                }catch(OggClip.InternalException ex){
                    ex.printStackTrace();
                }
                try{
                    bitStream.reset();
                }catch(IOException ex){
                    ex.printStackTrace();
                }
            };
        };
        player.setDaemon(true);
        player.start();
    }

    /**
     * Loop the clip - maybe for background music
     */
    public void loop(){
        stop();
        try{
            bitStream.reset();
        }catch(IOException ex){
            // ignore if no mark
        }
        player = new Thread(){
        
            @Override
            public void run(){
                while (player == Thread.currentThread()){
                    try{
                        playStream(Thread.currentThread());
                    }catch(OggClip.InternalException ex){
                        ex.printStackTrace();
                        player = null;
                    } 
                    try{
                        bitStream.reset();
                    }catch(IOException ex){
                    }
                }
            };
        };
        player.setDaemon(true);
        player.start();
    }

    /**
     * Stop the clip playing
     */
    public void stop(){
        if(stopped()){
            return;
        }
        player = null;
        outputLine.drain();
    }

    /**
     * Close the stream being played from
     */
    public void close(){
        try{
            if(bitStream != null){
                bitStream.close();
            }
        }catch(IOException ex){
        }
    }

    /*
     * Taken from the JOrbis Player
     */
    private void initJavaSound(int channels, int rate){
        try{
            AudioFormat audioFormat = new AudioFormat(rate, 16,
                channels, true, // PCM_Signed
                false // littleEndian
            );
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, AudioSystem.NOT_SPECIFIED);
            if(!AudioSystem.isLineSupported(info)){
                    throw new Exception("Line " + info + " not supported.");
            }
            try{
                outputLine = (SourceDataLine) AudioSystem.getLine(info);
                // outputLine.addLineListener(this);
                outputLine.open(audioFormat);
            }catch(LineUnavailableException ex){
                throw new Exception("Unable to open the sourceDataLine: " + ex);
            }catch(IllegalArgumentException ex){
                throw new Exception("Illegal Argument: " + ex);
            }
            this.rate = rate;
            this.channels = channels;
            setBalance(balance);
            setGain(gain);
        }catch(Exception ex){
            System.out.println(ex);
        }
    }

    /*
     * Taken from the JOrbis Player
     */
    private SourceDataLine getOutputLine(int channels, int rate){
        if((outputLine == null) || (this.rate != rate) || (this.channels != channels)){
            if (outputLine != null) {
                    outputLine.drain();
                    outputLine.stop();
                    outputLine.close();
            }
            initJavaSound(channels, rate);
            outputLine.start();
        }
        return outputLine;
    }

    /*
     * Taken from the JOrbis Player
     */
    private void initJOrbis(){
        oy=new SyncState();
        os=new StreamState();
        og=new Page();
        op=new Packet();
        vi=new Info();
        vc=new Comment();
        vd=new DspState();
        vb=new Block(vd);
        buffer=null;
        bytes=0;
        oy.init();
    }

    /*
     * Taken from the JOrbis Player
     */
    private void playStream(Thread me) throws OggClip.InternalException{
        boolean chained = false;
        initJOrbis();
        while(true){
                if(checkState()){
                    return;
                }
                int eos = 0;
                int index = oy.buffer(BUFSIZE);
                buffer = oy.data;
                try{
                    bytes = bitStream.read(buffer, index, BUFSIZE);
                }catch(Exception ex){
                    throw new OggClip.InternalException(ex);
                }
                oy.wrote(bytes);
                if(chained){ 
                    chained = false;  
                }
                else{ 
                    if(oy.pageout(og) != 1){
                        if (bytes < BUFSIZE){
                            break;
                        }
                        throw new OggClip.InternalException("Input does not appear to be an Ogg bitstream.");
                    }
                } 
                os.init(og.serialno());
                os.reset();
                vi.init();
                vc.init();
                if(os.pagein(og) < 0){
                    // error; stream version mismatch perhaps
                    throw new OggClip.InternalException("Error reading first page of Ogg bitstream data.");
                }
                if(os.packetout(op) != 1){
                    // no page? must not be vorbis
                    throw new OggClip.InternalException("Error reading initial header packet.");
                }
                if(vi.synthesis_headerin(vc, op) < 0){
                    // error case; not a vorbis header
                    throw new OggClip.InternalException("This Ogg bitstream does not contain Vorbis audio data.");
                }
                int i = 0;
                while(i < 2){
                    while(i < 2){
                        if(checkState()){
                                return;
                        }
                        int result = oy.pageout(og);
                        if(result == 0){
                                break; // Need more data
                        }
                        if(result == 1){
                            os.pagein(og);
                            while(i < 2){
                                result = os.packetout(op);
                                if (result == 0)
                                        break;
                                if (result == -1) {
                                        throw new OggClip.InternalException("Corrupt secondary header.  Exiting.");
                                }
                                vi.synthesis_headerin(vc, op);
                                i++;
                            }
                        }
                    }
                    index = oy.buffer(BUFSIZE);
                    buffer = oy.data;
                    try{
                        bytes = bitStream.read(buffer, index, BUFSIZE);
                    }catch(Exception ex){
                        throw new OggClip.InternalException(ex);
                    }
                    if((bytes == 0) && (i < 2)){
                        throw new OggClip.InternalException("End of file before finding all Vorbis headers!");
                    }
                    oy.wrote(bytes);
                }
                convsize = (BUFSIZE / vi.channels);
                vd.synthesis_init(vi);
                vb.init(vd);
                float[][][] _pcmf = new float[1][][];
                int[] _index = new int[vi.channels];
                getOutputLine(vi.channels, vi.rate);
                while(eos == 0){
                    while(eos == 0){
                        if(player != me){
                            return;
                        }
                        int result = oy.pageout(og);
                        if(result == 0){
                            break; // need more data
                        }
                        if(result == -1){ // missing or corrupt data at this page
                            // position
                            // System.err.println("Corrupt or missing data in
                            // bitstream;
                            // continuing...");
                        }
                        else{
                            os.pagein(og);
                            if (og.granulepos() == 0){
                                chained = true;
                                eos = 1;
                                break;
                            }
                            while(true){
                                if(checkState()){
                                    return;
                                }
                                result = os.packetout(op);
                                if(result == 0){
                                        break; // need more data
                                }
                                if(result == -1){ // missing or corrupt data at
                                    // this page position
                                    // no reason to complain; already complained
                                    // above

                                    // System.err.println("no reason to complain;
                                    // already complained above");
                                }
                                else{
                                    // we have a packet. Decode it
                                    int samples;
                                    if(vb.synthesis(op) == 0){ // test for
                                        // success!
                                        vd.synthesis_blockin(vb);
                                    }
                                    while((samples = vd.synthesis_pcmout(_pcmf, _index)) > 0){
                                        if(checkState()){
                                                return;
                                        }
                                        float[][] pcmf = _pcmf[0];
                                        int bout = ((samples < convsize)?samples:convsize);
                                        // convert doubles to 16 bit signed ints
                                        // (host order) and
                                        // interleave
                                        for(i=0;i<vi.channels;i++){
                                            int ptr = i * 2;
                                            // int ptr=i;
                                            int mono = _index[i];
                                            for(int j = 0; j < bout; j++){
                                                int val = (int) (pcmf[i][mono + j] * 32767.);
                                                if(val > 32767){
                                                        val = 32767;
                                                }
                                                if(val < -32768){
                                                        val = -32768;
                                                }
                                                if(val < 0){
                                                    val = val | 0x8000;
                                                }
                                                convbuffer[ptr] = (byte) (val);
                                                convbuffer[ptr + 1] = (byte) (val >>> 8);
                                                ptr += 2 * (vi.channels);
                                            }
                                        }
                                        outputLine.write(convbuffer, 0, 2 * vi.channels * bout);
                                        vd.synthesis_read(bout);
                                    }
                                }
                            }
                            if(og.eos() != 0){
                                eos = 1;
                            }
                        }
                    }
                    if(eos == 0){
                        index = oy.buffer(BUFSIZE);
                        buffer = oy.data;
                        try{
                            bytes = bitStream.read(buffer, index, BUFSIZE);
                        }catch(Exception ex){
                            throw new OggClip.InternalException(ex);
                        }
                        if(bytes == -1){
                            break;
                        }
                        oy.wrote(bytes);
                        if(bytes == 0){
                            eos = 1;
                        }
                    }
                }
                os.clear();
                vb.clear();
                vd.clear();
                vi.clear();
            }
            oy.clear();
    }

    private class InternalException extends Exception{
        
        public InternalException(Exception ex){
            super(ex);
        }

        public InternalException(String message){
            super(message);
        }
    }
}
